今天我想要來試著解說看看前兩天提到 Dialog, Prompt 概念時,有用到的官方範例程式碼。
首先先來看建構函示的部分。
class UserProfileDialog(ComponentDialog):
def __init__(self, user_state: UserState):
super(UserProfileDialog, self).__init__(UserProfileDialog.__name__)
self.user_profile_accessor = user_state.create_property("UserProfile")
self.add_dialog(
WaterfallDialog(
WaterfallDialog.__name__,
[
self.transport_step,
self.name_step,
self.name_confirm_step,
self.age_step,
self.picture_step,
self.confirm_step,
self.summary_step,
],
)
)
self.add_dialog(TextPrompt(TextPrompt.__name__))
self.add_dialog(
NumberPrompt(NumberPrompt.__name__, UserProfileDialog.age_prompt_validator)
)
self.add_dialog(ChoicePrompt(ChoicePrompt.__name__))
self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))
self.add_dialog(
AttachmentPrompt(
AttachmentPrompt.__name__, UserProfileDialog.picture_prompt_validator
)
)
self.initial_dialog_id = WaterfallDialog.__name__
這一行就是我們在介紹儲存使用者資料有提到的 accessor,以存取並修改對話中的資訊。
self.user_profile_accessor = user_state.create_property("UserProfile")
這幾個 add_dialog(),就是在宣告此 dialog(user_profile_dialog.py) 會用到哪些種類的 dialog/prompt 先加進來。
WaterfallDialog 的部分,因為是瀑布式 dialog,需要先把會遇到步驟先寫下來,先後順序也是在這裡設定,每個步驟的內容則是在底下每個 function 宣告時撰寫程式邏輯。
self.add_dialog(
WaterfallDialog(
WaterfallDialog.__name__,
[
self.transport_step,
self.name_step,
self.name_confirm_step,
self.age_step,
self.picture_step,
self.confirm_step,
self.summary_step,
],
)
)
self.add_dialog(TextPrompt(TextPrompt.__name__))
self.add_dialog(
NumberPrompt(NumberPrompt.__name__, UserProfileDialog.age_prompt_validator)
)
self.add_dialog(ChoicePrompt(ChoicePrompt.__name__))
self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))
self.add_dialog(
AttachmentPrompt(
AttachmentPrompt.__name__, UserProfileDialog.picture_prompt_validator
)
)
Dialog 的使用方法中,有一條規定,每個在 dialog 步驟的程式撰寫,都需要是另一個 dialog/prompt 做結尾,或是以 end_dialog() 做結尾。
async def transport_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
# WaterfallStep always finishes with the end of the Waterfall or with another dialog;
# here it is a Prompt Dialog. Running a prompt here means the next WaterfallStep will
# be run when the users response is received.
return await step_context.prompt(
ChoicePrompt.__name__,
PromptOptions(
prompt=MessageFactory.text("Please enter your mode of transport."),
choices=[Choice("Car"), Choice("Bus"), Choice("Bicycle")],
),
)
async def name_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
step_context.values["transport"] = step_context.result.value
return await step_context.prompt(
TextPrompt.__name__,
PromptOptions(prompt=MessageFactory.text("Please enter your name.")),
)
async def name_confirm_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
step_context.values["name"] = step_context.result
# We can send messages to the user at any point in the WaterfallStep.
await step_context.context.send_activity(
MessageFactory.text(f"Thanks {step_context.result}")
)
# WaterfallStep always finishes with the end of the Waterfall or
# with another dialog; here it is a Prompt Dialog.
return await step_context.prompt(
ConfirmPrompt.__name__,
PromptOptions(
prompt=MessageFactory.text("Would you like to give your age?")
),
)
async def age_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
if step_context.result:
# User said "yes" so we will be prompting for the age.
# WaterfallStep always finishes with the end of the Waterfall or with another dialog,
# here it is a Prompt Dialog.
return await step_context.prompt(
NumberPrompt.__name__,
PromptOptions(
prompt=MessageFactory.text("Please enter your age."),
retry_prompt=MessageFactory.text(
"The value entered must be greater than 0 and less than 150."
),
),
)
# User said "no" so we will skip the next step. Give -1 as the age.
return await step_context.next(-1)
async def picture_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
age = step_context.result
step_context.values["age"] = age
msg = (
"No age given."
if step_context.result == -1
else f"I have your age as {age}."
)
# We can send messages to the user at any point in the WaterfallStep.
await step_context.context.send_activity(MessageFactory.text(msg))
if step_context.context.activity.channel_id == "msteams":
# This attachment prompt example is not designed to work for Teams attachments, so skip it in this case
await step_context.context.send_activity(
"Skipping attachment prompt in Teams channel..."
)
return await step_context.next(None)
# WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt
# Dialog.
prompt_options = PromptOptions(
prompt=MessageFactory.text(
"Please attach a profile picture (or type any message to skip)."
),
retry_prompt=MessageFactory.text(
"The attachment must be a jpeg/png image file."
),
)
return await step_context.prompt(AttachmentPrompt.__name__, prompt_options)
async def confirm_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
step_context.values["picture"] = (
None if not step_context.result else step_context.result[0]
)
# WaterfallStep always finishes with the end of the Waterfall or
# with another dialog; here it is a Prompt Dialog.
return await step_context.prompt(
ConfirmPrompt.__name__,
PromptOptions(prompt=MessageFactory.text("Is this ok?")),
)
async def summary_step(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
if step_context.result:
# Get the current profile object from user state. Changes to it
# will saved during Bot.on_turn.
user_profile = await self.user_profile_accessor.get(
step_context.context, UserProfile
)
user_profile.transport = step_context.values["transport"]
user_profile.name = step_context.values["name"]
user_profile.age = step_context.values["age"]
user_profile.picture = step_context.values["picture"]
msg = f"I have your mode of transport as {user_profile.transport} and your name as {user_profile.name}."
if user_profile.age != -1:
msg += f" And age as {user_profile.age}."
await step_context.context.send_activity(MessageFactory.text(msg))
if user_profile.picture:
await step_context.context.send_activity(
MessageFactory.attachment(
user_profile.picture, "This is your profile picture."
)
)
else:
await step_context.context.send_activity(
"A profile picture was saved but could not be displayed here."
)
else:
await step_context.context.send_activity(
MessageFactory.text("Thanks. Your profile will not be kept.")
)
# WaterfallStep always finishes with the end of the Waterfall or with another
# dialog, here it is the end.
return await step_context.end_dialog()
首先也是看建構函式的部分,宣告了會用到的 dialog 以及 User state 及 Conversation state。
def __init__(
self,
conversation_state: ConversationState,
user_state: UserState,
dialog: Dialog,
):
if conversation_state is None:
raise TypeError(
"[DialogBot]: Missing parameter. conversation_state is required but None was given"
)
if user_state is None:
raise TypeError(
"[DialogBot]: Missing parameter. user_state is required but None was given"
)
if dialog is None:
raise Exception("[DialogBot]: Missing parameter. dialog is required")
self.conversation_state = conversation_state
self.user_state = user_state
self.dialog = dialog
這個 dialog_bot.py 沒有 on_members_added_activity() 所以剛啟用 chatbot 時,不會有歡迎訊息或是其他動作。on_message_activity() 則只有一個 DialogHelper.run_dialog(),代表這個機器人從一開始啟用就已經進入一個 Dialog,沒有其他動作。
async def on_message_activity(self, turn_context: TurnContext):
await DialogHelper.run_dialog(
self.dialog,
turn_context,
self.conversation_state.create_property("DialogState"),
)
這裡則有看到前幾天說明的 Storage,並且這邊用的是最簡單的 in-memory-storage,以及宣告 User state 及 Conversation state。
# Create MemoryStorage, UserState and ConversationState
MEMORY = MemoryStorage()
CONVERSATION_STATE = ConversationState(MEMORY)
USER_STATE = UserState(MEMORY)
這裡是宣告本次機器人要用的 dialog 以及主要的 bot.py。
# create main dialog and bot
DIALOG = UserProfileDialog(USER_STATE)
BOT = DialogBot(CONVERSATION_STATE, USER_STATE, DIALOG)
以上是我對這個程式碼一點分析心得,也是我當時第一次看比較難懂的地方,希望可以幫助大家節省開發的時間。